// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/midi/midi_manager_mac.h"

#include <algorithm>

#include "base/bind.h"
#include "base/message_loop/message_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"

#include <CoreAudio/HostTime.h>
#include <stddef.h>

using base::IntToString;
using base::SysCFStringRefToUTF8;
using midi::mojom::PortState;
using midi::mojom::Result;
using std::string;

// NB: System MIDI types are pointer types in 32-bit and integer types in
// 64-bit. Therefore, the initialization is the simplest one that satisfies both
// (if possible).

namespace midi {

namespace {

    // Maximum buffer size that CoreMIDI can handle for MIDIPacketList.
    const size_t kCoreMIDIMaxPacketListSize = 65536;
    // Pessimistic estimation on available data size of MIDIPacketList.
    const size_t kEstimatedMaxPacketDataSize = kCoreMIDIMaxPacketListSize / 2;

    MidiPortInfo GetPortInfoFromEndpoint(MIDIEndpointRef endpoint)
    {
        string manufacturer;
        CFStringRef manufacturer_ref = NULL;
        OSStatus result = MIDIObjectGetStringProperty(
            endpoint, kMIDIPropertyManufacturer, &manufacturer_ref);
        if (result == noErr) {
            manufacturer = SysCFStringRefToUTF8(manufacturer_ref);
        } else {
            // kMIDIPropertyManufacturer is not supported in IAC driver providing
            // endpoints, and the result will be kMIDIUnknownProperty (-10835).
            DLOG(WARNING) << "Failed to get kMIDIPropertyManufacturer with status "
                          << result;
        }

        string name;
        CFStringRef name_ref = NULL;
        result = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName,
            &name_ref);
        if (result == noErr) {
            name = SysCFStringRefToUTF8(name_ref);
        } else {
            DLOG(WARNING) << "Failed to get kMIDIPropertyDisplayName with status "
                          << result;
        }

        string version;
        SInt32 version_number = 0;
        result = MIDIObjectGetIntegerProperty(
            endpoint, kMIDIPropertyDriverVersion, &version_number);
        if (result == noErr) {
            version = IntToString(version_number);
        } else {
            // kMIDIPropertyDriverVersion is not supported in IAC driver providing
            // endpoints, and the result will be kMIDIUnknownProperty (-10835).
            DLOG(WARNING) << "Failed to get kMIDIPropertyDriverVersion with status "
                          << result;
        }

        string id;
        SInt32 id_number = 0;
        result = MIDIObjectGetIntegerProperty(
            endpoint, kMIDIPropertyUniqueID, &id_number);
        if (result == noErr) {
            id = IntToString(id_number);
        } else {
            // On connecting some devices, e.g., nano KONTROL2, unknown endpoints
            // appear and disappear quickly and they fail on queries.
            // Let's ignore such ghost devices.
            // Same problems will happen if the device is disconnected before finishing
            // all queries.
            DLOG(WARNING) << "Failed to get kMIDIPropertyUniqueID with status "
                          << result;
        }

        const PortState state = PortState::OPENED;
        return MidiPortInfo(id, manufacturer, name, version, state);
    }

    double MIDITimeStampToSeconds(MIDITimeStamp timestamp)
    {
        UInt64 nanoseconds = AudioConvertHostTimeToNanos(timestamp);
        return static_cast<double>(nanoseconds) / 1.0e9;
    }

    MIDITimeStamp SecondsToMIDITimeStamp(double seconds)
    {
        UInt64 nanos = UInt64(seconds * 1.0e9);
        return AudioConvertNanosToHostTime(nanos);
    }

} // namespace

MidiManager* MidiManager::Create()
{
    return new MidiManagerMac();
}

MidiManagerMac::MidiManagerMac()
    : midi_client_(0)
    , coremidi_input_(0)
    , coremidi_output_(0)
    , client_thread_("MidiClientThread")
    , shutdown_(false)
{
}

MidiManagerMac::~MidiManagerMac() = default;

void MidiManagerMac::StartInitialization()
{
    // MIDIClient should be created on |client_thread_| to receive CoreMIDI event
    // notifications.
    RunOnClientThread(
        base::Bind(&MidiManagerMac::InitializeCoreMIDI, base::Unretained(this)));
}

void MidiManagerMac::Finalize()
{
    // Wait for the termination of |client_thread_| before disposing MIDI ports.
    shutdown_ = true;
    client_thread_.Stop();

    if (coremidi_input_)
        MIDIPortDispose(coremidi_input_);
    if (coremidi_output_)
        MIDIPortDispose(coremidi_output_);
    if (midi_client_)
        MIDIClientDispose(midi_client_);
}

void MidiManagerMac::DispatchSendMidiData(MidiManagerClient* client,
    uint32_t port_index,
    const std::vector<uint8_t>& data,
    double timestamp)
{
    RunOnClientThread(
        base::Bind(&MidiManagerMac::SendMidiData,
            base::Unretained(this), client, port_index, data, timestamp));
}

void MidiManagerMac::RunOnClientThread(const base::Closure& closure)
{
    if (shutdown_)
        return;

    if (!client_thread_.IsRunning())
        client_thread_.Start();

    client_thread_.task_runner()->PostTask(FROM_HERE, closure);
}

void MidiManagerMac::InitializeCoreMIDI()
{
    DCHECK(client_thread_.task_runner()->BelongsToCurrentThread());

    // CoreMIDI registration.
    DCHECK_EQ(0u, midi_client_);
    OSStatus result = MIDIClientCreate(CFSTR("Chrome"), ReceiveMidiNotifyDispatch, this,
        &midi_client_);
    if (result != noErr || midi_client_ == 0)
        return CompleteInitialization(Result::INITIALIZATION_ERROR);

    // Create input and output port.
    DCHECK_EQ(0u, coremidi_input_);
    result = MIDIInputPortCreate(
        midi_client_,
        CFSTR("MIDI Input"),
        ReadMidiDispatch,
        this,
        &coremidi_input_);
    if (result != noErr || coremidi_input_ == 0)
        return CompleteInitialization(Result::INITIALIZATION_ERROR);

    DCHECK_EQ(0u, coremidi_output_);
    result = MIDIOutputPortCreate(
        midi_client_,
        CFSTR("MIDI Output"),
        &coremidi_output_);
    if (result != noErr || coremidi_output_ == 0)
        return CompleteInitialization(Result::INITIALIZATION_ERROR);

    // Following loop may miss some newly attached devices, but such device will
    // be captured by ReceiveMidiNotifyDispatch callback.
    uint32_t destination_count = MIDIGetNumberOfDestinations();
    destinations_.resize(destination_count);
    for (uint32_t i = 0; i < destination_count; i++) {
        MIDIEndpointRef destination = MIDIGetDestination(i);
        if (destination == 0) {
            // One ore more devices may be detached.
            destinations_.resize(i);
            break;
        }

        // Keep track of all destinations (known as outputs by the Web MIDI API).
        // Cache to avoid any possible overhead in calling MIDIGetDestination().
        destinations_[i] = destination;

        MidiPortInfo info = GetPortInfoFromEndpoint(destination);
        AddOutputPort(info);
    }

    // Open connections from all sources. This loop also may miss new devices.
    uint32_t source_count = MIDIGetNumberOfSources();
    for (uint32_t i = 0; i < source_count; ++i) {
        // Receive from all sources.
        MIDIEndpointRef source = MIDIGetSource(i);
        if (source == 0)
            break;

        // Start listening.
        MIDIPortConnectSource(
            coremidi_input_, source, reinterpret_cast<void*>(source));

        // Keep track of all sources (known as inputs in Web MIDI API terminology).
        source_map_[source] = i;

        MidiPortInfo info = GetPortInfoFromEndpoint(source);
        AddInputPort(info);
    }

    // Allocate maximum size of buffer that CoreMIDI can handle.
    midi_buffer_.resize(kCoreMIDIMaxPacketListSize);

    CompleteInitialization(Result::OK);
}

// static
void MidiManagerMac::ReceiveMidiNotifyDispatch(const MIDINotification* message,
    void* refcon)
{
    // This callback function is invoked on |client_thread_|.
    MidiManagerMac* manager = static_cast<MidiManagerMac*>(refcon);
    manager->ReceiveMidiNotify(message);
}

void MidiManagerMac::ReceiveMidiNotify(const MIDINotification* message)
{
    DCHECK(client_thread_.task_runner()->BelongsToCurrentThread());

    if (kMIDIMsgObjectAdded == message->messageID) {
        // New device is going to be attached.
        const MIDIObjectAddRemoveNotification* notification = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
        MIDIEndpointRef endpoint = static_cast<MIDIEndpointRef>(notification->child);
        if (notification->childType == kMIDIObjectType_Source) {
            // Attaching device is an input device.
            auto it = source_map_.find(endpoint);
            if (it == source_map_.end()) {
                MidiPortInfo info = GetPortInfoFromEndpoint(endpoint);
                // If the device disappears before finishing queries, MidiPortInfo
                // becomes incomplete. Skip and do not cache such information here.
                // On kMIDIMsgObjectRemoved, the entry will be ignored because it
                // will not be found in the pool.
                if (!info.id.empty()) {
                    uint32_t index = source_map_.size();
                    source_map_[endpoint] = index;
                    AddInputPort(info);
                    MIDIPortConnectSource(
                        coremidi_input_, endpoint, reinterpret_cast<void*>(endpoint));
                }
            } else {
                SetInputPortState(it->second, PortState::OPENED);
            }
        } else if (notification->childType == kMIDIObjectType_Destination) {
            // Attaching device is an output device.
            auto it = std::find(destinations_.begin(), destinations_.end(), endpoint);
            if (it == destinations_.end()) {
                MidiPortInfo info = GetPortInfoFromEndpoint(endpoint);
                // Skip cases that queries are not finished correctly.
                if (!info.id.empty()) {
                    destinations_.push_back(endpoint);
                    AddOutputPort(info);
                }
            } else {
                SetOutputPortState(it - destinations_.begin(), PortState::OPENED);
            }
        }
    } else if (kMIDIMsgObjectRemoved == message->messageID) {
        // Existing device is going to be detached.
        const MIDIObjectAddRemoveNotification* notification = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
        MIDIEndpointRef endpoint = static_cast<MIDIEndpointRef>(notification->child);
        if (notification->childType == kMIDIObjectType_Source) {
            // Detaching device is an input device.
            auto it = source_map_.find(endpoint);
            if (it != source_map_.end())
                SetInputPortState(it->second, PortState::DISCONNECTED);
        } else if (notification->childType == kMIDIObjectType_Destination) {
            // Detaching device is an output device.
            auto it = std::find(destinations_.begin(), destinations_.end(), endpoint);
            if (it != destinations_.end())
                SetOutputPortState(it - destinations_.begin(), PortState::DISCONNECTED);
        }
    }
}

// static
void MidiManagerMac::ReadMidiDispatch(const MIDIPacketList* packet_list,
    void* read_proc_refcon,
    void* src_conn_refcon)
{
    // This method is called on a separate high-priority thread owned by CoreMIDI.

    MidiManagerMac* manager = static_cast<MidiManagerMac*>(read_proc_refcon);
#if __LP64__
    MIDIEndpointRef source = reinterpret_cast<uintptr_t>(src_conn_refcon);
#else
    MIDIEndpointRef source = static_cast<MIDIEndpointRef>(src_conn_refcon);
#endif

    // Dispatch to class method.
    manager->ReadMidi(source, packet_list);
}

void MidiManagerMac::ReadMidi(MIDIEndpointRef source,
    const MIDIPacketList* packet_list)
{
    // This method is called from ReadMidiDispatch() and runs on a separate
    // high-priority thread owned by CoreMIDI.

    // Lookup the port index based on the source.
    auto it = source_map_.find(source);
    if (it == source_map_.end())
        return;
    // This is safe since MidiManagerMac does not remove any existing
    // MIDIEndpointRef, and the order is saved.
    uint32_t port_index = it->second;

    // Go through each packet and process separately.
    const MIDIPacket* packet = &packet_list->packet[0];
    for (size_t i = 0; i < packet_list->numPackets; i++) {
        // Each packet contains MIDI data for one or more messages (like note-on).
        double timestamp_seconds = MIDITimeStampToSeconds(packet->timeStamp);

        ReceiveMidiData(
            port_index,
            packet->data,
            packet->length,
            timestamp_seconds);

        packet = MIDIPacketNext(packet);
    }
}

void MidiManagerMac::SendMidiData(MidiManagerClient* client,
    uint32_t port_index,
    const std::vector<uint8_t>& data,
    double timestamp)
{
    DCHECK(client_thread_.task_runner()->BelongsToCurrentThread());

    // Lookup the destination based on the port index.
    if (static_cast<size_t>(port_index) >= destinations_.size())
        return;

    MIDITimeStamp coremidi_timestamp = SecondsToMIDITimeStamp(timestamp);
    MIDIEndpointRef destination = destinations_[port_index];

    size_t send_size;
    for (size_t sent_size = 0; sent_size < data.size(); sent_size += send_size) {
        MIDIPacketList* packet_list = reinterpret_cast<MIDIPacketList*>(midi_buffer_.data());
        MIDIPacket* midi_packet = MIDIPacketListInit(packet_list);
        // Limit the maximum payload size to kEstimatedMaxPacketDataSize that is
        // half of midi_buffer data size. MIDIPacketList and MIDIPacket consume
        // extra buffer areas for meta information, and available size is smaller
        // than buffer size. Here, we simply assume that at least half size is
        // available for data payload.
        send_size = std::min(data.size() - sent_size, kEstimatedMaxPacketDataSize);
        midi_packet = MIDIPacketListAdd(
            packet_list,
            kCoreMIDIMaxPacketListSize,
            midi_packet,
            coremidi_timestamp,
            send_size,
            &data[sent_size]);
        DCHECK(midi_packet);

        MIDISend(coremidi_output_, destination, packet_list);
    }

    AccumulateMidiBytesSent(client, data.size());
}

} // namespace midi
